package wasnake.grid;

import java.io.Serializable;
import java.util.ArrayList;
import static wasnake.grid.Direction.*;

/**
 * A <code>Location</code> object represents the row and column of a location
 * in a two-dimensional grid. <br />
 *
 * CREDIT TO: GridWorld case study
 */
public class Location implements Comparable, Serializable {
    /** The turn angle for turning 90 degrees to the left. */
    public static final int LEFT = -90;
    /** The turn angle for turning 90 degrees to the right. */
    public static final int RIGHT = 90;
    /** The turn angle for turning 45 degrees to the left. */
    public static final int HALF_LEFT = -45;
    /** The turn angle for turning 45 degrees to the right. */
    public static final int HALF_RIGHT = 45;
    /** The turn angle for turning a full circle. */
    public static final int FULL_CIRCLE = 360;
    /** The turn angle for turning a half circle. */
    public static final int HALF_CIRCLE = 180;
    /** Row location in grid*/
    protected int row;
    /** Column location in grid*/
    protected int col;

    /**
     * Constructs a location with given row and column coordinates.
     * @param r the row
     * @param c the column
     */
    public Location(int r, int c) {
        row = r;
        col = c;
    }

    /**
     * Gets the row coordinate.
     * @return the row of this location
     */
    public int getRow(){ return row;}
    /**
     * Gets the column coordinate.
     * @return the column of this location
     */
    public int getCol() { return col; }

    /**
     * Gets the adjacent location in any one of the eight compass directions.
     * @param direction the direction in which to find a neighbor location
     * @return the adjacent location the given direction
     */
    public Location getAdjacentLocation(Direction d) {
        // dc and dr represent the delta in column and row to the new location
        int dc = 0;
        int dr = 0;

        switch (d) {
            case NORTH:
                dr = -1;
                break;
            case NORTHEAST:
                dc = 1;
                dr = -1;
                break;
            case EAST:
                dc = 1;
                break;
            case SOUTHEAST:
                dc = 1;
                dr = 1;
                break;
            case SOUTH:
                dr = 1;
                break;
            case SOUTHWEST:
                dc = -1;
                dr = 1;
                break;
            case WEST:
                dc = -1;
                break;
            case NORTHWEST:
                dc = -1;
                dr = -1;
                break;
            default:
                throw new IllegalArgumentException(d +
                        " is not a recognized direction.");
        }
        
        return new Location(getRow() + dr, getCol() + dc);
    }

    /**
     * Returns the direction from this location toward another location. The
     * direction is rounded to the nearest compass direction.
     * @param target a location that is different from this location
     * @return the closest compass direction from this location toward
     * <code>target</code>
     */
    public Direction getDirectionToward(Location target) {
        int dx = target.getCol() - getCol();
        int dy = target.getRow() - getRow();
        // y axis points opposite to mathematical orientation
        int angle = (int) Math.toDegrees(Math.atan2(-dy, dx));

        // mathematical angle is counterclockwise from x-axis,
        // compass angle is clockwise from y-axis
        int compassAngle = RIGHT - angle;
        // prepare for truncating division by 45 degrees
        compassAngle += HALF_RIGHT / 2;
        // wrap negative angles
        if (compassAngle < 0)
            compassAngle += FULL_CIRCLE;
        // round to nearest multiple of 45, and return the direction
        return map((compassAngle / HALF_RIGHT) * HALF_RIGHT);
    }

    /**
     * Indicates whether some other <code>Location</code> object is "equal to"
     * this one.
     * @param other the other location to test
     * @return <code>true</code> if <code>other</code> is a
     * <code>Location</code> with the same row and column as this location;
     * <code>false</code> otherwise
     */
    @Override
    public boolean equals(Object other) {
        if (!(other instanceof Location))
            return false;

        Location otherLoc = (Location) other;
        return getRow() == otherLoc.getRow() && getCol() == otherLoc.getCol();
    }

    /**
     * Generates a hash code.
     * @return a hash code for this location
     */
    @Override
    public int hashCode() { return getRow() * 3737 + getCol(); }

    /**
     * Compares this location to <code>other</code> for ordering. Returns a
     * negative integer, zero, or a positive integer as this location is less
     * than, equal to, or greater than <code>other</code>. Locations are
     * ordered in row-major order. <br />
     * (Precondition: <code>other</code> is a <code>Location</code> object.)
     * @param other the other location to test
     * @return a negative integer if this location is less than
     * <code>other</code>, zero if the two locations are equal, or a positive
     * integer if this location is greater than <code>other</code>
     */
    @Override
    public int compareTo(Object other){
        Location otherLoc = (Location) other;
        if (getRow() < otherLoc.getRow())
            return -1;
        if (getRow() > otherLoc.getRow())
            return 1;
        if (getCol() < otherLoc.getCol())
            return -1;
        if (getCol() > otherLoc.getCol())
            return 1;
        return 0;
    }

    /**
     * @param gr the bounded grid from which to get the line of locations.
     * @param dir the direction from this location in which to get the line of
     * locations.
     * @return all locations in the given direction from this location.
     */
    public ArrayList<Location> getLocationsInLine(
            AbstractGrid gr, Direction dir){
        ArrayList<Location> list = new ArrayList<Location>();

        Location nextMove = getAdjacentLocation(dir);
        while (gr.isValid(nextMove)) {
            list.add(nextMove);
            nextMove = nextMove.getAdjacentLocation(dir);
        }
        return list;
    }

    /**
     * Creates a string that describes this location.
     * @return a string with the row and column of this location, in the format
     * (row, col)
     */
    @Override
    public String toString() { return "(" + getRow() + ", " + getCol() + ")"; }
}